package de.jmda.core.util.fileset;

import java.io.File;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlElements;
import javax.xml.bind.annotation.XmlRootElement;

/**
 * Base class for file sets.
 * <p>
 * Provides a set of files via {@link #calculateFileSet()} (ordinary files and
 * directories may be included in the set). The set is calculated based on rules
 * to first include and then optionally exclude files. The rules will be
 * evaluated in the following order whenever {@link #calculateFileSet()} is
 * called.
 * <ul>
 *   <li>{@link #includeFile(File, RuleSetUpdateMode)}
 *   <li>{@link #includeDirectory(File, FileFilterBase, RuleSetUpdateMode)}
 *   <li>{@link #includeDirectory(File, RuleSetUpdateMode)}
 *   <li>{@link #includeDirectoryRecursive(File, FileFilterBase, RuleSetUpdateMode)}
 *   <li>{@link #includeDirectoryRecursive(File, RuleSetUpdateMode)}
 *   <li>{@link #excludeFile(File, RuleSetUpdateMode)}
 *   <li>{@link #excludeDirectory(File, FileFilterBase, RuleSetUpdateMode)}
 *   <li>{@link #excludeDirectory(File, RuleSetUpdateMode)}
 *   <li>{@link #excludeDirectoryRecursive(File, FileFilterBase, RuleSetUpdateMode)}
 *   <li>{@link #excludeDirectoryRecursive(File, RuleSetUpdateMode)}
 * </ul>
 */
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class FileSet
{
	/**
	 * Value is used in <code>package-info</code> and defines the XML namespace
	 * for this and other JAXB classes in this package.
	 */
	public final static String XML_NAMESPACE = "http://code.google.com/p/jmda/core";

	/**
	 * Indicates if an update operation adds or removes a rule to / from a rule
	 * set.
	 */
	public enum RuleSetUpdateMode
	{
		ADD_RULE,
		REMOVE_RULE
	}

	/**
	 * Indicates if a rule includes or excludes files to / from the file set.
	 */
	private enum ProcessingMode
	{
		INCLUDE,
		EXCLUDE
	}

	//	private final static Logger LOGGER = ClassUtil.getClassLogger();

	//	private final static String FILE_SET_RULES =
	//			FileSet.class.getSimpleName() + "Rules";

	@XmlElementWrapper
	@XmlElements(@XmlElement(name="includeFileRule"))
	private final Set<File> includeFileRules = new TreeSet<File>();

	@XmlElementWrapper
	@XmlElements(@XmlElement(name="excludeFileRule"))
	private final Set<File> excludeFileRules = new TreeSet<File>();

	@XmlElementWrapper
	@XmlElements(@XmlElement(name="includeDirectoryRule"))
	private final Set<File> includeDirectoryRules = new TreeSet<File>();

	@XmlElementWrapper
	@XmlElements(@XmlElement(name="excludeDirectoryRule"))
	private final Set<File> excludeDirectoryRules = new TreeSet<File>();

	@XmlElementWrapper
	@XmlElements(@XmlElement(name="includeDirectoryFilterRule"))
	private final Set<DirectoryFileFilter> includeDirectoryFilterRules =
	new TreeSet<DirectoryFileFilter>();

	@XmlElementWrapper
	@XmlElements(@XmlElement(name="excludeDirectoryFilterRule"))
	private final Set<DirectoryFileFilter> excludeDirectoryFilterRules =
	new TreeSet<DirectoryFileFilter>();

	@XmlElementWrapper
	@XmlElements(@XmlElement(name="includeDirectoryRecursiveRule"))
	private final Set<File> includeDirectoryRecursiveRules = new TreeSet<File>();

	@XmlElementWrapper
	@XmlElements(@XmlElement(name="excludeDirectoryRecursiveRule"))
	private final Set<File> excludeDirectoryRecursiveRules = new TreeSet<File>();

	@XmlElementWrapper
	@XmlElements(@XmlElement(name="includeDirectoryFilterRecursiveRule"))
	private final Set<DirectoryFileFilter> includeDirectoryFilterRecursiveRules =
	new TreeSet<DirectoryFileFilter>();

	@XmlElementWrapper
	@XmlElements(@XmlElement(name="excludeDirectoryFilterRecursiveRule"))
	private final Set<DirectoryFileFilter> excludeDirectoryFilterRecursiveRules =
	new TreeSet<DirectoryFileFilter>();

	public Set<File> getExcludeDirectoryRules()
	{
		return Collections.unmodifiableSet(excludeDirectoryRules);
	}

	public Set<File> getExcludeDirectoryRecursiveRules()
	{
		return Collections.unmodifiableSet(excludeDirectoryRecursiveRules);
	}

	public Set<DirectoryFileFilter> getExcludeDirectoryFilterRules()
	{
		return Collections.unmodifiableSet(excludeDirectoryFilterRecursiveRules);
	}

	public Set<DirectoryFileFilter> getExcludeDirectoryFilterRecursiveRules()
	{
		return Collections.unmodifiableSet(excludeDirectoryFilterRecursiveRules);
	}

	public Set<File> getExcludeFileRules()
	{
		return Collections.unmodifiableSet(excludeFileRules);
	}

	public Set<File> getIncludeDirectoryRules()
	{
		return Collections.unmodifiableSet(includeDirectoryRules);
	}

	public Set<File> getIncludeDirectoryRecursiveRules()
	{
		return Collections.unmodifiableSet(includeDirectoryRecursiveRules);
	}

	public Set<DirectoryFileFilter> getIncludeDirectoryFilterRules()
	{
		return Collections.unmodifiableSet(includeDirectoryFilterRules);
	}

	public Set<DirectoryFileFilter> getIncludeDirectoryFilterRecursiveRules()
	{
		return Collections.unmodifiableSet(includeDirectoryFilterRecursiveRules);
	}

	public Set<File> getIncludeFileRules()
	{
		return Collections.unmodifiableSet(includeFileRules);
	}

	/**
	 * @return calculated set of files
	 * @see FileSet
	 */
	public Set<File> calculateFileSet()
	{
		Set<File> result =
				new TreeSet<File>(
						new Comparator<File>()
						{
							@Override
							public int compare(File file1, File file2)
							{
								return
										file1.getAbsolutePath().compareTo(file2.getAbsolutePath());
							}
						});

		// add files to include to the file set
		result.addAll(includeFileRules);

		for (DirectoryFileFilter directoryFilter : includeDirectoryFilterRules)
		{
			processDirectoryContent(
					result,
					directoryFilter.getDirectory(),
					directoryFilter.getFileFilter(), ProcessingMode.INCLUDE, false);
		}

		for (File file : includeDirectoryRules)
		{
			processDirectoryContent(
					result, file, null, ProcessingMode.INCLUDE, false);
		}

		for (DirectoryFileFilter directoryFilter :
			includeDirectoryFilterRecursiveRules)
		{
			processDirectoryContent(
					result,
					directoryFilter.getDirectory(),
					directoryFilter.getFileFilter(), ProcessingMode.INCLUDE, true);
		}

		for (File file : includeDirectoryRecursiveRules)
		{
			processDirectoryContent(result, file, null, ProcessingMode.INCLUDE, true);
		}

		// remove files to exclude from the file set
		for (File file : excludeFileRules)
		{
			result.remove(file);
		}

		for (DirectoryFileFilter directoryFilter : excludeDirectoryFilterRules)
		{
			processDirectoryContent(
					result,
					directoryFilter.getDirectory(),
					directoryFilter.getFileFilter(), ProcessingMode.EXCLUDE, false);
		}

		for (File file : excludeDirectoryRules)
		{
			processDirectoryContent(result, file, null, ProcessingMode.EXCLUDE, false);
		}

		for (DirectoryFileFilter directoryFilter :
			excludeDirectoryFilterRecursiveRules)
		{
			processDirectoryContent(
					result,
					directoryFilter.getDirectory(),
					directoryFilter.getFileFilter(), ProcessingMode.EXCLUDE, true);
		}

		for (File file : excludeDirectoryRecursiveRules)
		{
			processDirectoryContent(result, file, null, ProcessingMode.EXCLUDE, true);
		}

		return result;
	}

	public static List<String> fileSet2AbsolutePathStringList(Set<File> fileset)
	{
		List<String> result = new ArrayList<String>();

		for (File file : fileset)
		{
			result.add(file.getAbsolutePath());
		}

		return result;
	}

	/**
	 * Adds / removes a rule to include a concrete <code>file</code> (ordinary and
	 * directory files allowed) to the file set.
	 *
	 * @param file
	 * @param ruleSetUpdateMode
	 */
	public void includeFile(File file, RuleSetUpdateMode ruleSetUpdateMode)
	{
		if (ruleSetUpdateMode == RuleSetUpdateMode.ADD_RULE)
		{
			includeFileRules.add(file);
		}
		else
		{
			includeFileRules.remove(file);
		}
	}

	/**
	 * Adds / removes a rule to exclude a concrete <code>file</code> (ordinary and
	 * directory files allowed) from the file set.
	 *
	 * @param file
	 * @param ruleSetUpdateMode
	 */
	public void excludeFile(File file, RuleSetUpdateMode ruleSetUpdateMode)
	{
		if (ruleSetUpdateMode == RuleSetUpdateMode.ADD_RULE)
		{
			excludeFileRules.add(file);
		}
		else
		{
			excludeFileRules.remove(file);
		}
	}

	/**
	 * Adds / removes a rule to include a <code>directory</code>'s files to the
	 * file set. Includes the directory itself and it's ordinary files but
	 * does not include it's subdirectories and their ordinary files.
	 *
	 * @param directory
	 * @param ruleSetUpdateMode
	 */
	public void includeDirectory(
			File directory, RuleSetUpdateMode ruleSetUpdateMode)
	{
		if (directory.isDirectory())
		{
			if (ruleSetUpdateMode == RuleSetUpdateMode.ADD_RULE)
			{
				includeDirectoryRules.add(directory);
			}
			else
			{
				includeDirectoryRules.remove(directory);
			}
		}
	}

	/**
	 * Adds / removes a rule to exclude a <code>directory</code>'s files from the
	 * file set. Excludes the directory itself and it's ordinary files but leaves
	 * it's subdirectories and their files in the file set.
	 *
	 * @param directory
	 * @param ruleSetUpdateMode
	 */
	public void excludeDirectory(
			File directory, RuleSetUpdateMode ruleSetUpdateMode)
	{
		if (directory.isDirectory())
		{
			if (ruleSetUpdateMode == RuleSetUpdateMode.ADD_RULE)
			{
				excludeDirectoryRules.add(directory);
			}
			else
			{
				excludeDirectoryRules.remove(directory);
			}
		}
	}

	/**
	 * Adds / removes a rule to include a <code>directory</code>'s files to the
	 * file set. Includes the directory itself and it's ordinary files but
	 * does not include it's subdirectories and their ordinary files. Files are
	 * only added if they match the filter's criteria.
	 *
	 * @param directory
	 * @param filter
	 * @param ruleSetUpdateMode
	 */
	public void includeDirectory(
			File directory,
			FileFilterBase filter,
			RuleSetUpdateMode ruleSetUpdateMode)
	{
		if (directory.isDirectory())
		{
			if (ruleSetUpdateMode == RuleSetUpdateMode.ADD_RULE)
			{
				includeDirectoryFilterRules.add(
						new DirectoryFileFilter(directory, filter));
			}
			else
			{
				includeDirectoryFilterRules.remove(
						new DirectoryFileFilter(directory, filter));
			}
		}
	}

	/**
	 * Adds / removes a rule to exclude a <code>directory</code>'s files from the
	 * file set. Excludes the directory itself and it's ordinary files but leaves
	 * it's subdirectories and their ordinary files in the set. Files are only
	 * excluded if they match the filter's criteria.
	 *
	 * @param directory
	 * @param filter
	 * @param ruleSetUpdateMode
	 */
	public void excludeDirectory(
			File directory,
			FileFilterBase filter,
			RuleSetUpdateMode ruleSetUpdateMode)
	{
		if (directory.isDirectory())
		{
			if (ruleSetUpdateMode == RuleSetUpdateMode.ADD_RULE)
			{
				excludeDirectoryFilterRules.add(
						new DirectoryFileFilter(directory, filter));
			}
			else
			{
				excludeDirectoryFilterRules.remove(
						new DirectoryFileFilter(directory, filter));
			}
		}
	}

	/**
	 * Adds / removes a rule to include a <code>directory</code>'s files to the
	 * file set recursively. Includes the directory itself, it's ordinary files,
	 * it's subdirectories and their files.
	 *
	 * @param directory
	 * @param ruleSetUpdateMode
	 */
	public void includeDirectoryRecursive(
			File directory, RuleSetUpdateMode ruleSetUpdateMode)
	{
		if (directory.isDirectory())
		{
			if (ruleSetUpdateMode == RuleSetUpdateMode.ADD_RULE)
			{
				includeDirectoryRecursiveRules.add(directory);
			}
			else
			{
				includeDirectoryRecursiveRules.remove(directory);
			}
		}
	}

	/**
	 * Adds / removes a rule to include a <code>directory</code>'s files to the
	 * file set recursively. Includes the directory itself, it's ordinary files,
	 * it's subdirectories and their files. Files are only included if they match
	 * the filter's criteria.
	 *
	 * @param directory
	 * @param filter
	 * @param ruleSetUpdateMode
	 */
	public void includeDirectoryRecursive(
			File directory,
			FileFilterBase filter,
			RuleSetUpdateMode ruleSetUpdateMode)
	{
		if (directory.isDirectory())
		{
			if (ruleSetUpdateMode == RuleSetUpdateMode.ADD_RULE)
			{
				includeDirectoryFilterRecursiveRules.add(
						new DirectoryFileFilter(directory, filter));
			}
			else
			{
				includeDirectoryFilterRecursiveRules.remove(
						new DirectoryFileFilter(directory, filter));
			}
		}
	}

	/**
	 * Adds / removes a rule to exclude a <code>directory</code>'s files from the
	 * file set recursively. Excludes the directory itself, it's ordinary files,
	 * it's subdirectories and their files.
	 *
	 * @param directory
	 * @param ruleSetUpdateMode
	 */
	public void excludeDirectoryRecursive(
			File directory, RuleSetUpdateMode ruleSetUpdateMode)
	{
		if (directory.isDirectory())
		{
			if (ruleSetUpdateMode == RuleSetUpdateMode.ADD_RULE)
			{
				excludeDirectoryRecursiveRules.add(directory);
			}
			else
			{
				excludeDirectoryRecursiveRules.remove(directory);
			}
		}
	}

	/**
	 * Adds / removes a rule to exclude a <code>directory</code>'s files from the
	 * file set recursively. Excludes the directory itself, it's ordinary files,
	 * it's subdirectories and their files. Files are only excluded if they match
	 * the filter's criteria.
	 *
	 * @param directory
	 * @param filter
	 * @param ruleSetUpdateMode
	 */
	public void excludeDirectoryRecursive(
			File directory,
			FileFilterBase filter,
			RuleSetUpdateMode ruleSetUpdateMode)
	{
		if (directory.isDirectory())
		{
			if (ruleSetUpdateMode == RuleSetUpdateMode.ADD_RULE)
			{
				excludeDirectoryFilterRecursiveRules.add(
						new DirectoryFileFilter(directory, filter));
			}
			else
			{
				excludeDirectoryFilterRecursiveRules.remove(
						new DirectoryFileFilter(directory, filter));
			}
		}
	}

	/**
	 * @param fileset set of file to be added to / removed from
	 * @param directory that will be added to / removed from <code>fileset</code>
	 *        depending on <code>mode</code>
	 * @param fileFilter that will be applied before files will be added to /
	 *        removed from the file set. Files that match the filter criteria will
	 *        not be added / removed.
	 * @param mode controls whether files will be added or removed
	 * @param recurse controls whether directories will be processed recursively
	 */
	private void processDirectoryContent(
			Set<File>      fileset,
			File           directory,
			FileFilter     fileFilter,
			ProcessingMode mode,
			boolean        recurse)
	{
		// add or remove the directory to process itself
		processFile(fileset, directory, fileFilter, mode);

		// add or remove the content of the directory (do not list files using
		// fileFilter here, if a file will be processed or not will be decided in
		// process methods)
		for (File file : directory.listFiles())
		{
			if (recurse)
			{
				if (file.isDirectory())
				{
					// call this method recursively
					processDirectoryContent(fileset, file, fileFilter, mode, recurse);
				}
				else
				{
					processFile(fileset, file, fileFilter, mode);
				}
			}
			else
			{
				// contained directories will be ignored
				if (file.isFile())
				{
					processFile(fileset, file, fileFilter, mode);
				}
			}
		}
	}

	private void processFile(
			Set<File>      fileset,
			File           file,
			FileFilter     fileFilter,
			ProcessingMode mode)
	{
		boolean processFile = true;

		if (fileFilter != null)
		{
			if (!(fileFilter.accept(file)))
			{
				processFile = false;
			}
		}

		if (processFile)
		{
			if (mode == ProcessingMode.INCLUDE)
			{
				fileset.add(file);
			}
			else
			{
				fileset.remove(file);
			}
		}
	}

	/**
	 * @param directory
	 * @return all ordinary and directory files found in <code>directory</code>
	 *         and sub directories of <code>directory</code>
	 */
	@SuppressWarnings("unused")
	private Set<File> calculateContainedFilesRecursively(File directory)
	{
		Set<File> result = new TreeSet<File>();

		for (File file : directory.listFiles())
		{
			result.add(file);

			if (file.isDirectory())
			{
				result.addAll(calculateContainedFilesRecursively(file));
			}
		}

		return result;
	}
}